[Generic表]TDRBlob内嵌Protobuf部分字段读写
[TDR Generic表][C++ SDK]TDR Blob内嵌Protobuf部分字段读写
混合字段路径的综合读写示例、路径语法及限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 C++ 的
TdrPbFieldGroup、SetAllTdrPbFieldNames,以及完整的 Service API 请求与响应示例。
1. 接口说明
TDR 表的 Blob 字段(char 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据, 可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。
三个命令对应三个命令字:
| 命令 | 命令字 | 值 | 说明 |
|---|---|---|---|
| FieldGet | TCAPLUS_API_PB_FIELD_GET_REQ | 0x0067 | 读取 Blob 中指定的 PB 字段 |
| FieldSet | TCAPLUS_API_PB_FIELD_SET_REQ | 0x0069 | 更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入 |
| BatchFieldGet | TCAPLUS_API_PB_BATCH_FIELD_GET_REQ | 0x0075 | 多个主键共用一组字段路径批量读取 |
Blob 中 Protobuf 字段的自增(TCAPLUS_API_PB_FIELD_INCREASE_REQ)在 TDR 表上不支持。
本特性的字段名适配层是可选头文件 tcaplus_tdr_pb_fields.h,只替换这一行:
request->SetFieldNames(field_names, field_count);
其余请求和响应流程保持 Service API 原有用法不变。
不 include 该头文件的 TDR 用户不会被强制依赖 protobuf。
2. 版本要求
- Service API 3.55.0 SP06(3.55.0.209070)及以上;
- 服务端须为 Tcapsvr 3.73.0 2026-10-10 以后的版本。旧版后端在这三个命令上返回
SVR_ERR_FAIL_TO_FIND_META,需要升级后端。
3. 准备工作
参见准备工作文档,完成使用该接口前的准备工作,并创建 TDR Generic 表。
本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段。下面以 RoleSummaryTable 为例:
<struct name="Col" version="1">
<entry name="len" type="uint" defaultvalue="0" desc="bin 的有效长度" />
<entry name="bin" type="char" count="1024" refer="len" desc="PB 数据" />
</struct>
<struct name="RoleSummaryTable" version="1" primarykey="id" splittablekey="id">
<entry name="id" type="int64" desc="角色ID" />
<entry name="del" type="int" desc="删除标记" />
<entry name="col1" type="Col" desc="保存 ProfileBaseData 的 Blob" />
</struct>
col1.bin 是保存 PB 的 Blob 字段,col1.len 是它的 refer 长度字段。
注意:字段路径里用 TDR 表定义的字段名(col1.bin、col1.len、del)。
Blob 里的 PB 定义示例如下(ProfileBaseData后面都是使用该proto进行示例演示):
syntax = "proto3";
package tcaplus_demo;
message ProfileBaseData {
uint32 level = 5;
string plat_name = 6;
repeated uint32 equipped_tag_list = 16;
map<int32, BanInfo> ban_info_map = 22;
map<string, string> settings = 23;
}
message BanInfo { string info = 1; int32 duration = 2; }
该proto表定义不用上传到Tcaplus服务端,只存在于客户端
完整示例见 examples/tcaplus/C++_tdr2.0_tdr_pb_fields:
| 内容 | 路径 |
|---|---|
| 表定义 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/table_test.xml |
| PB 定义 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/profile_base_data.proto |
| 示例代码 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp |
| 编译与运行 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/readme.txt |
示例里的三个函数分别对应下面 5.1、5.2、5.3,响应处理对应 5.4。
4. PB部分字段操作方式
4.1. 方案1
部分字段读写的核心是字段路径,服务端没有业务的 .proto,只认数字 tag 路径,直接用点分式写PB数字tagid:
// 操作PB ProfileBaseData 的 level字段
// 操作PB ProfileBaseData 的 plat_name字段
// 操作PB ProfileBaseData 的 equipped_tag_list 字段(proto3数组默认 packed,只能整数组读写)
// 操作PB ProfileBaseData 的 ban_info_map 字段的map元素1001
// 操作PB ProfileBaseData 的 settings 字段的map元素region
// 操作tdr原生字段del
const char* field_names[] = {"col1.bin.5", "col1.bin.6", "col1.bin.16", "col1.bin.22[1001]", "col1.bin.23['region']", "del"};
ret = request->SetFieldNames(field_names, sizeof(field_names) / sizeof(field_names[0]));
4.2. 方案2
直接使用tagid操作字段,不够直观,维护不方便,因此我们增加了PB字段名到 tagid 映射的适配层用法,方便维护。
字段名到 tag 路径的转换用 tcaplus_tdr_pb_fields.h 里的适配层:
#include "tcaplus_tdr_pb_fields.h"
const char* col1_fields[] = {
"level", // 操作PB ProfileBaseData 的 level字段
"plat_name", // 操作PB ProfileBaseData 的 plat_name字段
"equipped_tag_list", // 操作PB ProfileBaseData 的 equipped_tag_list 字段(proto3数组默认 packed,只能整数组读写)
"ban_info_map[1001]", // 操作PB ProfileBaseData 的 ban_info_map 字段的map元素1001
"settings['region']" // 操作PB ProfileBaseData 的 settings 字段的map元素region
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
// 操作PB字段 ProfileBaseData
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),
// 如果有tdr中有多个Blob存放了PB Message,比如col2.bin,可以用多个 TdrPbFieldGroup
// TcaplusService::TdrPbFieldGroup(
// "col2.bin", xxx::descriptor(), col2_fields),
// 操作原生 TDR 字段,原样透传
TcaplusService::TdrPbFieldGroup("del")
};
// 最终调用会转换成方案1的调用
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
5. 完整示例代码
示例代码见发布包 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp,对应函数为 SendProfileFieldSet、SendProfileFieldGet、SendProfileBatchFieldGet 和 HandleProfileFieldResponse,入口在 main() 中。
5.1 FieldSet(部分更新)
int32_t SendProfileFieldSet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
const char kTableName[] = "RoleSummaryTable";
// 1. 创建并初始化原生 Service API request
TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
if (request == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_SET_REQ);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 2. 业务构造增量 PB message
tcaplus_demo::ProfileBaseData delta;
delta.set_level(100);
delta.set_plat_name("plat_x");
(*delta.mutable_ban_info_map())[1001].set_info("ban info");
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_id; // 主键
// 3. PB增量message序列化到TDR Blob
const size_t encoded_size = delta.ByteSizeLong();
if (encoded_size > sizeof(row.stCol1.szBin))
{
return TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
}
if (!delta.SerializePartialToArray(row.stCol1.szBin, static_cast<int>(encoded_size)))
{
return TcapErrCode::API_ERR_PACK_MESSAGE;
}
row.stCol1.dwLen = static_cast<uint32_t>(encoded_size);
// 4. 继续使用原生 record
TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
if (record == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
/////////////////////////////////////////////////////////////////////
// 5. 唯一被适配的步骤:按 PB 字段名设置部分字段集合
const char* col1_fields[] = {
"level",
"plat_name",
"ban_info_map[1001]"
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 或者使用方案1的tagid方式调用
// const char* field_names[] = {"col1.bin.5", "col1.bin.6", "col1.bin.22[1001]"};
// ret = request->SetFieldNames(field_names, 3);
///////////////////////////////////////////////////////////////////
return server.SendRequest(request);
}
5.2 FieldGet(部分读取,同时读原生 TDR 字段)
int32_t SendProfileFieldGet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
const char kTableName[] = "RoleSummaryTable";
// 1. 创建并初始化原生 Service API request
TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
if (request == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_GET_REQ);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 2. 只填主键,value 字段由服务端按字段路径回填
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_id; // 主键
// 3. 继续使用原生 record
TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
if (record == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
/////////////////////////////////////////////////////////////////////
// 4. 唯一被适配的步骤:按 PB 字段名设置部分字段集合
const char* col1_fields[] = {
"level",
"plat_name",
"ban_info_map[1001].info",
"settings['region']"
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),
// 原生 TDR 字段不经过 PB 解析,原样透传
TcaplusService::TdrPbFieldGroup("del")
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 或者使用方案1的tagid方式调用
// const char* field_names[] = {"col1.bin.5", "col1.bin.6", "col1.bin.22[1001].1",
// "col1.bin.23['region']", "del"};
// ret = request->SetFieldNames(field_names, 5);
///////////////////////////////////////////////////////////////////
return server.SendRequest(request);
}
5.3 BatchFieldGet(批量读取)
int32_t SendProfileBatchFieldGet(
TcaplusService::TcaplusServer& server,
const int64_t role_ids[],
unsigned role_count)
{
const char kTableName[] = "RoleSummaryTable";
// 1. 创建并初始化原生 Service API request
TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
if (request == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_BATCH_FIELD_GET_REQ);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 2. 每条记录只填主键,一次请求最多 1024 条
if (role_ids == NULL || role_count == 0 || role_count > 1024)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
for (unsigned i = 0; i < role_count; ++i)
{
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_ids[i]; // 主键
TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
if (record == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
}
/////////////////////////////////////////////////////////////////////
// 3. 唯一被适配的步骤:所有记录共用同一组字段路径,只设置一次
const char* col1_fields[] = {"level", "plat_name"};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 或者使用方案1的tagid方式调用
// const char* field_names[] = {"col1.bin.5", "col1.bin.6"};
// ret = request->SetFieldNames(field_names, 2);
///////////////////////////////////////////////////////////////////
return server.SendRequest(request);
}
SetAllTdrPbFieldNames() 最终设置的是 col1.bin.5 和 col1.bin.6。
与 5.2 的 FieldGet 一样只填主键,区别是一次请求带多条;所有记录共用同一组字段路径, SetAllTdrPbFieldNames() 只调用一次,单次最多 1024 条主键。 批量请求里每条记录各自返回结果,某条主键不存在或字段缺失只影响它自己, 处理响应时逐条看 FetchRecord() 的返回值(见 5.4)。
批量读取还需注意:
- 不支持条件。 在批量读取请求的 record 上调用
SetCondition()会返回GEN_ERR_INVALID_ARGUMENTS。 - 响应可能分包。 响应包超过 256KB 时服务端会分包返回,分包不会切分单条记录,且无法关闭。收到一个分包后,用
TcaplusServiceResponse::HaveMoreResPkgs()判断是否还有下一个包,返回 1 时继续调用RecvResponse()收包,直到所有分包处理完毕;GetRecordCount()只返回当前分包中的记录数。
5.4 响应处理
响应端不使用适配层,三个命令共用同一份处理逻辑:直接读取 TDR,再解析 Blob 里的部分 PB。
int32_t HandleProfileFieldResponse(TcaplusService::TcaplusServiceResponse* response)
{
// 1. 先看整个请求的结果
if (response == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = response->GetResult();
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 2. 逐条取记录:单条失败不影响其它记录,这里只记录第一个错误
int32_t first_error = TcapErrCode::GEN_ERR_SUC;
const int record_count = response->GetRecordCount();
for (int i = 0; i < record_count; ++i)
{
// 2.1 取记录,批量请求里这是这一条自己的结果
const TcaplusService::TcaplusServiceRecord* record = NULL;
ret = response->FetchRecord(record);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = ret;
}
continue;
}
// 2.2 取回 TDR 结构,Blob 里已经是本次的部分 PB
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
ret = record->GetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = ret;
}
continue;
}
// 2.3 长度字段是本次部分 PB 的长度,业务自己校验上界
if (row.stCol1.dwLen > sizeof(row.stCol1.szBin))
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
}
continue;
}
// 2.4 只解析本次返回的部分 PB
tcaplus_demo::ProfileBaseData profile;
if (!profile.ParsePartialFromArray(row.stCol1.szBin, static_cast<int>(row.stCol1.dwLen)))
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = TcapErrCode::API_ERR_UNPACK_MESSAGE;
}
continue;
}
// 2.5 profile 只包含本次请求返回的部分字段,不能当作完整记录使用
ConsumeProfile(row.llId, row.iDel, profile);
}
return first_error;
}
响应端不使用适配层,流程和原生 Service API 完全一致,只有 Blob 的解析由业务自己做。有以下几点需要注意:
row.stCol1.dwLen是本次部分 PB 的长度,不是记录里完整 Blob 的长度;- 解析出的
profile只包含本次请求的字段,不能再拿去整段覆盖 Blob; - 批量请求里每条记录各自返回结果,逐条处理时以
FetchRecord()的返回值判断这一条是否成功; - 批量读取的响应可能分包,需要对每个分包调用该函数,直到
HaveMoreResPkgs()不再返回 1(见 5.3); - 该函数在
GetResult()非 0 时直接返回,适用于读取。FieldSet 失败时如需取回 ResultFlag 指定的数据,见 5.6。
5.5 删除 map 元素
删除 map 中指定 key 的元素时,在字段名前加 POP 和空格。POP 专用于删除 map 的指定 key。请求按 5.1 的方式构造,区别是不需要增量 PB:只填主键,Blob 留空,长度字段填 0。
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_id; // 主键
row.stCol1.dwLen = 0; // 删除不需要增量 PB
// ... Init(TCAPLUS_API_PB_FIELD_SET_REQ)、AddRecord()、SetData() 同 5.1 ...
const char* col1_fields[] = {"POP ban_info_map[1001]"};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
// 或者使用方案1的tagid方式调用
// const char* field_names[] = {"POP col1.bin.22[1001]"};
// ret = request->SetFieldNames(field_names, 1);
删除后 ban_info_map 中其他 key 保留;删除不存在的 key 也返回成功。同一个 key 不能在一个请求里既更新又删除。
5.6 条件与 ResultFlag
条件。 FieldGet 和 FieldSet 在 AddRecord() 返回的 record 上调用 SetCondition(),放在 SetData() 和设置字段路径之后。条件中的 PB 路径使用 tagid,适配层不转换条件文本。BatchFieldGet 不支持条件(见 5.3)。
ret = record->SetCondition("del = 0");
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
FieldSet 条件不成立时返回 COMMON_ERR_CONDITION_NOT_MATCHED,记录和版本都不变。
ResultFlag。 FieldSet 使用 SetResultFlagForSuccess() 和 SetResultFlagForFail() 分别控制成功、失败时的返回数据,取值含义见《TDR Blob 内嵌 Protobuf 部分字段读写》4.5 节。C++ 需要注意:
- 两侧都不设置时,成功响应会回带请求中的字段,即整段增量 Blob。只关心执行结果时,建议设置
SetResultFlagForSuccess(0)。 - 失败响应只有在显式调用
SetResultFlagForFail()时才能取到记录。此时即使GetResult()非 0,只要GetRecordCount()大于 0,就可以FetchRecord()后GetData()。未显式设置时,失败响应不带记录。
// 发送前:成功时不返回数据,失败时返回失败前的完整记录
ret = request->SetResultFlagForSuccess(0);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;
ret = request->SetResultFlagForFail(3);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;
// 收到 TCAPLUS_API_PB_FIELD_SET_RES 后
int32_t result = response->GetResult();
if (result != TcapErrCode::GEN_ERR_SUC && response->GetRecordCount() > 0)
{
const TcaplusService::TcaplusServiceRecord* record = NULL;
if (response->FetchRecord(record) == TcapErrCode::GEN_ERR_SUC && record != NULL)
{
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
int32_t version = 0;
if (record->GetData(&row, sizeof(row), &version) == TcapErrCode::GEN_ERR_SUC)
{
// row 是失败前的完整记录,col1.bin 中是完整 PB;可据此决定是否重试
}
}
}
return result;
SetResultFlagForFail(3) 只有在服务端读到记录之后才失败时(条件不成立、版本冲突、map key 缺失、下标越界等)才带回记录;记录不存在、已过期,或请求在读取记录前就被拒绝时,失败响应不带记录。
6. 适配层错误码
本地检查(SetAllTdrPbFieldNames() 返回值):
| 场景 | 返回值 |
|---|---|
| request 或字段数组为空、字段路径为空,或字段路径语法错误 | API_ERR_PARAMETER_INVALID |
| descriptor 中不存在字段名或 tag | API_ERR_FIELD_NOT_EXSIST |
对 packed 数组写下标;写范围下标(如 tags[0-1]);一条路径访问两层容器元素;在非 map、非 repeated 字段上写下标;路径穿过 repeated、map 或标量字段继续访问子字段 | API_ERR_FIELD_TYPE_NOT_MATCH |
| map key 写法不符:字符串 key 未加引号且不是十六进制;整数 key 不是十进制或超出 key 类型的取值范围;key 类型不支持按 key 访问 | API_ERR_FIELD_TYPE_NOT_MATCH |
| 转换成功,但路径重复或父子路径冲突 | SetFieldNames() 返回的 API_ERR_PARAMETER_INVALID |
| request 未初始化、字段数过多、完整路径过长 | 原样返回 SetFieldNames() 的错误码 |
转换失败时不会给 request 留下部分字段,换用正确的字段分组重新调用即可。
发送后的结果看 response->GetResult(),批量读取还要看每次 FetchRecord() 的返回值。常见服务端错误码:
| 错误码 | 含义 |
|---|---|
TXHDB_ERR_RECORD_NOT_EXIST | 记录不存在或已过期;FieldSet 不会自动插入记录 |
COMMON_ERR_ELEMENT_NOT_EXIST(537) | 指定的 map key 不存在,或 FieldSet 更新 map 指定 key 时增量中缺少该 key;repeated 下标越界也可能返回该错误码。该错误码是正数,不要用 ret < 0 判断失败,应判断 ret != 0 |
COMMON_ERR_INVALID_ARRAY_INDEX | repeated 下标越界或下标写法不合法 |
COMMON_ERR_CONDITION_NOT_MATCHED | 条件不成立 |
SVR_ERR_FAIL_INVALID_VERSION | 版本校验失败 |
SVR_ERR_FAIL_INVALID_OPERATION | FieldSet 不支持通过 SetOperation() 携带更新表达式 |
SVR_ERR_FAIL_PROTOBUF_FIELD_GET / SVR_ERR_FAIL_PROTOBUF_FIELD_UPDATE | Blob 中不是合法的 PB 编码,或无法按给定路径读取、更新(如路径类型与实际数据不符) |
SVR_ERR_FAIL_TO_FIND_META | 后端版本过旧,不支持本功能,需要升级后端(见第 2 节) |
详见错误码含义和处理方法。
7. 注意事项
- 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
- Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
- 返回的
col1.len是本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。 - SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责, 适配层只解决
PB field name -> field number的映射。 - 路径里的一级字段名取 TDR 表定义中的字段名,不是 C++ 结构体字段名。
- 字段名使用
.proto中的原始 field name,不使用 JSON name。 - 头文件依赖完整 protobuf descriptor API,传入
NULL只表示本次按数字 tag 转换; 不 include 该头文件的 TDR 用户不会被引入 protobuf 依赖。 - 嵌套结构体里的 Blob(如本例的
col1.bin)也支持,补齐的是嵌套结构体字段本身。